-- -------------------------------------------------------------------------
-- Features/Spawn.lua
-- Vehicle Spawner
-- -------------------------------------------------------------------------

local module = {}

local MODULE_NAME <const> = "Spawn"

-- [Lua 5.4] <const> for function references

local math_sin <const> = math.sin
local math_cos <const> = math.cos
local math_rad <const> = math.rad
local pcall <const> = pcall
local ipairs <const> = ipairs

-- [Dependencies]
local ui = nil
local natives = nil
local lang = nil
local vehicles_data = nil
local vehicle_customizer = nil
local vehicle_converter = nil
local debugger = nil
local features_config = nil

-- [Translation Helper]
local function _T(text)
    if _G and _G._T then
        return _G._T(text)
    end
    if lang then
        if lang.get then return lang.get(text) end
        if lang.get_text then return lang.get_text(text) end
    end
    return text
end

local function translate_list(list)
    local options = {}
    for i, value in ipairs(list) do
        options[i] = _T(value)
    end
    return options
end

-- [Configuration Constants]
-- UI options (will be translated at runtime)
local MAIN_TABS <const> = { "Vehicles", "Saved Vehicles" }
local COLOR_MODES <const> = { "Classic Colors", "Custom RGB", "Chameleon Paint" }

-- [Natives Cache]
local N = {}

-- [External Dependencies]
-- Load JSON library
local json = require("ClickUl.Lib.json")

-- Forward declarations
local scan_vehicle_files

-- [Module State]
local state = {
    -- Spawn options
    teleport_to_vehicle = true,
    delete_old_vehicle = true,
    spawn_in_air = false,
    engine_on = true,
    max_performance = false,
    max_upgrade = false,
    clean_vehicle = true,
    bulletproof_tires = false,
    vehicle_godmode = false,

    -- Preset options
    use_preset = false,

    -- Color presets
    color_mode = 1,             -- 1=Classic, 2=Custom RGB, 3=Chameleon
    primary_color = 1,
    secondary_color = 1,
    primary_color_rgb = {r=0, g=0, b=0, a=255},
    secondary_color_rgb = {r=0, g=0, b=0, a=255},
    pearlescent_id = 0,
    wheel_color_id = 0,
    chameleon_paint = 1,

    -- Light presets
    xenon_color = 1,
    neon_color = 1,

    -- Wheel presets
    wheel_type = 1,
    wheel_index = 0,
    custom_tires = false,
    tire_smoke_color = 1,

    -- Window & plate
    window_tint = 1,
    plate_type = 1,
    plate_text = "",

    -- Runtime state
    loading = false,
    last_spawned = nil,

    -- UI state
    main_tab = 1,
    search_query = "",

    -- External vehicles
    external_vehicles = {},
    external_loaded = false,
    search_text = "",
    filter_stand = true,
    filter_cherax = true,
    filter_lexis = true,
    custom_page = 1,
    custom_page_size = 20,

    -- Save vehicle
    save_vehicle_name = "",
}

-- Cached dropdown options (populated in init)
local cached_options = {
    paint_names = nil,
    xenon_names = nil,
    neon_names = nil,
    smoke_names = nil,
    wheel_type_names = nil,
    chameleon_names = nil,
    -- Translated UI arrays
    main_tabs = nil,
    color_modes = nil,
}

-- Config Save/Load Helpers
local function load_config()
    if not features_config then return end

    local saved = features_config.get_module(MODULE_NAME)
    if not saved then return end

    -- Load spawn options
    if saved.teleport_to_vehicle ~= nil then state.teleport_to_vehicle = saved.teleport_to_vehicle end
    if saved.delete_old_vehicle ~= nil then state.delete_old_vehicle = saved.delete_old_vehicle end
    if saved.spawn_in_air ~= nil then state.spawn_in_air = saved.spawn_in_air end
    if saved.engine_on ~= nil then state.engine_on = saved.engine_on end
    if saved.max_performance ~= nil then state.max_performance = saved.max_performance end
    if saved.max_upgrade ~= nil then state.max_upgrade = saved.max_upgrade end
    if saved.clean_vehicle ~= nil then state.clean_vehicle = saved.clean_vehicle end
    if saved.bulletproof_tires ~= nil then state.bulletproof_tires = saved.bulletproof_tires end
    if saved.vehicle_godmode ~= nil then state.vehicle_godmode = saved.vehicle_godmode end

    -- Load preset options
    if saved.use_preset ~= nil then state.use_preset = saved.use_preset end
    if saved.color_mode ~= nil then state.color_mode = saved.color_mode end
    if saved.primary_color ~= nil then state.primary_color = saved.primary_color end
    if saved.secondary_color ~= nil then state.secondary_color = saved.secondary_color end
    if saved.chameleon_paint ~= nil then state.chameleon_paint = saved.chameleon_paint end
    if saved.pearlescent_id ~= nil then state.pearlescent_id = saved.pearlescent_id end
    if saved.wheel_color_id ~= nil then state.wheel_color_id = saved.wheel_color_id end
    if saved.xenon_color ~= nil then state.xenon_color = saved.xenon_color end
    if saved.neon_color ~= nil then state.neon_color = saved.neon_color end
    if saved.wheel_type ~= nil then state.wheel_type = saved.wheel_type end
    if saved.wheel_index ~= nil then state.wheel_index = saved.wheel_index end
    if saved.custom_tires ~= nil then state.custom_tires = saved.custom_tires end
    if saved.tire_smoke_color ~= nil then state.tire_smoke_color = saved.tire_smoke_color end
    if saved.window_tint ~= nil then state.window_tint = saved.window_tint end
    if saved.plate_type ~= nil then state.plate_type = saved.plate_type end
    if saved.plate_text ~= nil then state.plate_text = saved.plate_text end

    -- Load RGB colors
    if saved.primary_color_rgb and type(saved.primary_color_rgb) == "table" then
        state.primary_color_rgb = saved.primary_color_rgb
    end
    if saved.secondary_color_rgb and type(saved.secondary_color_rgb) == "table" then
        state.secondary_color_rgb = saved.secondary_color_rgb
    end

    if debugger then debugger.debug("[Spawn] Config loaded") end
end

local function save_config()
    if not features_config then return end

    features_config.set_module(MODULE_NAME, {
        teleport_to_vehicle = state.teleport_to_vehicle,
        delete_old_vehicle = state.delete_old_vehicle,
        spawn_in_air = state.spawn_in_air,
        engine_on = state.engine_on,
        max_performance = state.max_performance,
        max_upgrade = state.max_upgrade,
        clean_vehicle = state.clean_vehicle,
        bulletproof_tires = state.bulletproof_tires,
        vehicle_godmode = state.vehicle_godmode,
        use_preset = state.use_preset,
        color_mode = state.color_mode,
        primary_color = state.primary_color,
        secondary_color = state.secondary_color,
        primary_color_rgb = state.primary_color_rgb,
        secondary_color_rgb = state.secondary_color_rgb,
        chameleon_paint = state.chameleon_paint,
        pearlescent_id = state.pearlescent_id,
        wheel_color_id = state.wheel_color_id,
        xenon_color = state.xenon_color,
        neon_color = state.neon_color,
        wheel_type = state.wheel_type,
        wheel_index = state.wheel_index,
        custom_tires = state.custom_tires,
        tire_smoke_color = state.tire_smoke_color,
        window_tint = state.window_tint,
        plate_type = state.plate_type,
        plate_text = state.plate_text,
    }, true)
end

-- [Fuzzy Search]
--- Check if query characters appear in order within text
---@param text string Text to search
---@param query string Search keyword
---@return boolean Match result
local function fuzzy_match(text, query)
    if not query or query == "" then return true end
    if not text or type(text) ~= "string" then return false end

    local text_lower = text:lower()
    local query_lower = query:lower()

    local text_idx = 1
    local text_len = #text_lower
    local query_len = #query_lower

    for i = 1, query_len do
        local char = query_lower:sub(i, i)
        local found = false

        while text_idx <= text_len do
            if text_lower:sub(text_idx, text_idx) == char then
                text_idx = text_idx + 1
                found = true
                break
            end
            text_idx = text_idx + 1
        end

        if not found then
            return false
        end
    end

    return true
end

-- [Vehicle Name Localization]
--- Get localized vehicle name
---@param model_name string Vehicle model name (e.g., "adder")
---@return string Localized name
local function get_localized_vehicle_name(model_name)
    if not model_name or model_name == "" then return model_name end

    local hash = joaat(model_name)

    -- Step 1: Get GXT label (e.g., "ADDER")
    if N.GET_DISPLAY_NAME_FROM_VEHICLE_MODEL then
        local label = N.GET_DISPLAY_NAME_FROM_VEHICLE_MODEL(hash)

        if label and label ~= "" and label ~= "CARNOTFOUND" then
            -- Step 2: Get localized text from label (e.g., "Adder" or localized name)
            if N.GET_LABEL_TEXT then
                local ok, result = pcall(N.GET_LABEL_TEXT, label)
                if ok and result and result ~= "" then
                    return result  -- Localized name
                end
            end
            return label  -- Fallback to GXT label
        end
    end

    return model_name  -- Fallback to original model name
end

-- [Initialization]
function module.init(ui_lib, _, nat_lib, _, _, _, debug_lib)
    ui = ui_lib
    natives = nat_lib
    debugger = debug_lib

    -- Cache all used native functions
    if natives then
        local function get(name) return natives[name] end

        -- Player/Entity
        N.PLAYER_PED_ID = get("PLAYER_PED_ID")
        N.GET_ENTITY_COORDS = get("GET_ENTITY_COORDS")
        N.GET_ENTITY_HEADING = get("GET_ENTITY_HEADING")
        N.DOES_ENTITY_EXIST = get("DOES_ENTITY_EXIST")
        N.SET_ENTITY_AS_MISSION_ENTITY = get("SET_ENTITY_AS_MISSION_ENTITY")

        -- Vehicle creation/model
        N.CREATE_VEHICLE = get("CREATE_VEHICLE")
        N.SET_MODEL_AS_NO_LONGER_NEEDED = get("SET_MODEL_AS_NO_LONGER_NEEDED")
        N.IS_THIS_MODEL_A_PLANE = get("IS_THIS_MODEL_A_PLANE")
        N.IS_THIS_MODEL_A_HELI = get("IS_THIS_MODEL_A_HELI")
        N.HAVE_VEHICLE_MODS_STREAMED_IN = get("HAVE_VEHICLE_MODS_STREAMED_IN")

        -- Vehicle control
        N.SET_PED_INTO_VEHICLE = get("SET_PED_INTO_VEHICLE")
        N.SET_VEHICLE_ENGINE_ON = get("SET_VEHICLE_ENGINE_ON")
        N.IS_PED_IN_ANY_VEHICLE = get("IS_PED_IN_ANY_VEHICLE")
        N.GET_VEHICLE_PED_IS_IN = get("GET_VEHICLE_PED_IS_IN")

        -- Localization
        N.GET_DISPLAY_NAME_FROM_VEHICLE_MODEL = get("GET_DISPLAY_NAME_FROM_VEHICLE_MODEL")
        N.GET_LABEL_TEXT = get("GET_LABEL_TEXT")
    end

    -- Load vehicle data
    local success, data = pcall(require, "ClickUl.Data.vehicles")
    if success and data then
        vehicles_data = data
        if debugger then
            debugger.info("[Spawn] Loaded " .. #data.CATEGORIES .. " vehicle categories")
        end

        -- Pre-cache localized vehicle names (one-time cost at init)
        local localized_count = 0
        for _, category in ipairs(data.CATEGORIES) do
            local vehicles = data.VEHICLES[category]
            if vehicles then
                for _, vehicle in ipairs(vehicles) do
                    -- Add display_name field with localized name
                    local localized = get_localized_vehicle_name(vehicle.model)
                    if localized and localized ~= vehicle.model then
                        vehicle.display_name = localized
                        localized_count = localized_count + 1
                    else
                        vehicle.display_name = vehicle.name  -- Fallback to original name
                    end
                end
            end
        end
        if debugger then
            debugger.info("[Spawn] Localized " .. localized_count .. " vehicle names")
        end
    else
        if debugger then
            debugger.error("[Spawn] Failed to load vehicle data")
        end
        return false
    end

    -- Load vehicle customizer
    local vc_success, vc = pcall(require, "ClickUl.Utils.VehicleCustomizer")
    if vc_success and vc then
        vehicle_customizer = vc
        -- Initialize with natives and debugger (Self.lua pattern)
        local init_success, init_err = pcall(function()
            vehicle_customizer.init(natives, debugger)
        end)
        if init_success then
            if debugger then debugger.info("[Spawn]", "VehicleCustomizer loaded and initialized") end
            -- Pre-cache dropdown options (built once)
            cached_options.paint_names = vehicle_customizer.build_dropdown_options(vehicle_customizer.PAINT_PRESETS)
            cached_options.xenon_names = vehicle_customizer.build_dropdown_options(vehicle_customizer.XENON_PRESETS)
            cached_options.neon_names = vehicle_customizer.build_dropdown_options(vehicle_customizer.NEON_PRESETS)
            cached_options.smoke_names = vehicle_customizer.build_dropdown_options(vehicle_customizer.SMOKE_PRESETS)
            cached_options.wheel_type_names = vehicle_customizer.build_dropdown_options(vehicle_customizer.WHEEL_TYPES)
            cached_options.chameleon_names = vehicle_customizer.build_dropdown_options(vehicle_customizer.CHAMELEON_PRESETS)
            cached_options.color_modes = translate_list(COLOR_MODES)
        else
            print("[Spawn] WARNING: VehicleCustomizer init failed: " .. tostring(init_err))
            -- Continue without customizer
            vehicle_customizer = nil
        end
    else
        print("[Spawn] WARNING: VehicleCustomizer not available: " .. tostring(vc))
    end

    -- Load features config module
    local ok, fc = pcall(require, "ClickUl.Config.features")
    if ok and fc then
        features_config = fc
        -- Load saved config
        load_config()
    end

    -- Auto-load custom vehicles (delayed execution to ensure dirs API is ready)
    util.create_job(function()
        util.yield(500)  -- Wait 500ms to ensure game environment is fully initialized
        if dirs and dirs.list then
            pcall(scan_vehicle_files)
        end
    end)

    cached_options.main_tabs = translate_list(MAIN_TABS)

    return true
end

-- [External Vehicle File Parsers]
--- Parse Stand vehicle file (.txt format)
---@param file_path string Path to Stand .txt file
---@return table|nil data Parsed vehicle data or nil on error
local function parse_stand_file(file_path)
    -- Use Lexis file.open API
    if not file or not file.open then
        if debugger then debugger.warn("[Spawn] file API not available") end
        return nil
    end

    local content = nil
    local ok, result = pcall(function()
        if file.exists and not file.exists(file_path) then
            return nil
        end

        local f = file.open(file_path, {create_if_not_exists=false, append=false})
        if f and f.valid then
            local text = f.text
            if f.close then
                pcall(f.close, f)
            elseif f.Close then
                pcall(f.Close, f)
            end
            return text
        end
        return nil
    end)

    if ok and result then
        content = result
    else
        if debugger then debugger.warn("[Spawn] Failed to open Stand file: " .. file_path) end
        return nil
    end

    if not content or content == "" then
        if debugger then debugger.warn("[Spawn] Empty Stand file: " .. file_path) end
        return nil
    end

    local data = {}
    for line in content:gmatch("[^\r\n]+") do
        if line:find(":") then
            local key, value = line:match("^(.-):%s*(.*)$")
            if key and value then
                -- Clean whitespace
                key = key:gsub("^%s*(.-)%s*$", "%1")
                value = value:gsub("^%s*(.-)%s*$", "%1")

                -- Special handling for wheel values (e.g., "10 - Street")
                if key == "Front Wheels" or key == "Back Wheels" then
                    local wheel_num = value:match("^(%d+)")
                    data[key] = wheel_num and tonumber(wheel_num) or 0
                -- Type conversion
                elseif tonumber(value) then
                    data[key] = tonumber(value)
                elseif value:lower() == "yes" then
                    data[key] = true
                elseif value:lower() == "no" then
                    data[key] = false
                else
                    data[key] = value
                end
            end
        end
    end

    return next(data) and data or nil
end

--- Parse Cherax vehicle file (.json format)
---@param file_path string Path to Cherax .json file
---@return table|nil data Parsed vehicle data or nil on error
local function parse_cherax_file(file_path)
    -- Use Lexis file.open API
    if not file or not file.open then
        if debugger then debugger.warn("[Spawn] file API not available") end
        return nil
    end

    local content = nil
    local ok, result = pcall(function()
        if file.exists and not file.exists(file_path) then
            return nil
        end

        local f = file.open(file_path, {create_if_not_exists=false, append=false})
        if f and f.valid then
            local text = f.text
            if f.close then
                pcall(f.close, f)
            elseif f.Close then
                pcall(f.Close, f)
            end
            return text
        end
        return nil
    end)

    if ok and result then
        content = result
    else
        if debugger then debugger.warn("[Spawn] Failed to open Cherax file: " .. file_path) end
        return nil
    end

    if not content or content == "" then
        if debugger then debugger.warn("[Spawn] Empty Cherax file: " .. file_path) end
        return nil
    end

    local parse_ok, data = pcall(json.decode, content)
    if not parse_ok or not data then
        if debugger then debugger.warn("[Spawn] Failed to parse Cherax JSON: " .. file_path) end
        return nil
    end

    -- Validate Cherax format
    if data.format ~= "Cherax Entity" then
        if debugger then debugger.warn("[Spawn] Invalid Cherax format: " .. file_path) end
        return nil
    end

    return data
end

--- Parse Lexis vehicle file (.json format)
---@param file_path string Path to Lexis .json file
---@return table|nil data Parsed vehicle data or nil on error
local function parse_lexis_file(file_path)
    if not file or not file.open then
        if debugger then debugger.warn("[Spawn] file API not available") end
        return nil
    end

    local content = nil
    local ok, result = pcall(function()
        if file.exists and not file.exists(file_path) then
            return nil
        end

        local f = file.open(file_path, {create_if_not_exists=false, append=false})
        if f and f.valid then
            local text = f.text
            if f.close then
                pcall(f.close, f)
            elseif f.Close then
                pcall(f.Close, f)
            end
            return text
        end
        return nil
    end)

    if ok and result then
        content = result
    else
        return nil
    end

    if not content or content == "" then
        return nil
    end

    local parse_ok, data = pcall(json.decode, content)
    if not parse_ok or not data then
        return nil
    end

    -- Detect Lexis format (has specific fields)
    if not data.model or not data.mods or not data["wheel type"] then
        return nil
    end

    return data
end

--- Extract folder name from file path
---@param path string Full file path
---@param base_path string Base directory path
---@return string folder Folder name or "Root"
local function extract_folder_name(path, base_path)
    local relative = path:sub(#base_path + 1)
    local folder = relative:match("^(.-)[\\/][^\\/]+$")
    return folder and folder:gsub("^[\\/]+", ""):gsub("[\\/]+$", "") or "Root"
end

--- Recursively scan directory for files with extension
---@param dir string Directory path
---@param extension string File extension (e.g., "txt", "json")
---@return table files List of file paths
local function scan_directory(dir, extension)
    local files = {}

    -- Use Lexis dirs.list API (call directly without pcall)
    if dirs and dirs.list then
        local result = dirs.list(dir, extension)
        if result and type(result) == "table" then
            for _, path in ipairs(result) do
                table.insert(files, path)
            end
            if debugger then
                debugger.debug("[Spawn] Found " .. #files .. " files in " .. dir)
            end
        end
    end

    return files
end

--- Load all external vehicles (unified)
function load_external_vehicles()
    if debugger then debugger.info("[Spawn] Loading external vehicles...") end

    -- Use paths.script to get the correct base path
    local base = (paths and paths.script) or ".\\"
    if not base:match("[\\/]$") then base = base .. "\\" end

    state.external_vehicles = {}

    -- Load external vehicle config files
    local config_ok, config = pcall(require, "ClickUl.Data.external_vehicles")
    if not config_ok or not config then
        if debugger then
            debugger.warn("[Spawn] External vehicles config not found or invalid")
        end
        return
    end

    local stand_path = base .. config.STAND_PATH
    local cherax_path = base .. config.CHERAX_PATH

    -- Load Stand vehicles from config list
    for _, filename in ipairs(config.STAND_VEHICLES or {}) do
        local file_path = stand_path .. filename .. ".txt"

        local ok_parse, data = pcall(parse_stand_file, file_path)

        if ok_parse and data then
            -- Try multiple possible field names
            local model_name = data["Model Name"] or data["Model"] or data["model"] or data["Vehicle"]

            if model_name then
                table.insert(state.external_vehicles, {
                    name = model_name,
                    display_name = filename,
                    path = file_path,
                    model = model_name,
                    data = data,
                    format = "Stand",
                    folder = "Root",
                    source = "Stand"
                })
            end
        end
    end

    -- Load Cherax vehicles from config list
    for _, filename in ipairs(config.CHERAX_VEHICLES or {}) do
        local file_path = cherax_path .. filename .. ".json"

        local ok_parse, data = pcall(parse_cherax_file, file_path)
        if ok_parse and data and data.model then
            table.insert(state.external_vehicles, {
                name = data.name or filename,
                display_name = filename,
                path = file_path,
                model = data.model,
                data = data,
                format = "Cherax",
                folder = "Root",
                source = "Cherax"
            })
        end
    end

    -- Sort by name
    local ok_sort, err_sort = pcall(function()
        table.sort(state.external_vehicles, function(a, b)
            return a.name:lower() < b.name:lower()
        end)
    end)

    if not ok_sort and debugger then
        debugger.warn("[Spawn] Error sorting vehicles: " .. tostring(err_sort))
    end

    if debugger then
        debugger.info("[Spawn] Loaded " .. #state.external_vehicles .. " custom vehicles")
    end
end

-- [Core Functions]
-- Get player's current vehicle
local function get_current_vehicle()
    local ped = N.PLAYER_PED_ID and N.PLAYER_PED_ID()
    if not ped or ped == 0 then return nil end
    if not N.IS_PED_IN_ANY_VEHICLE or not N.IS_PED_IN_ANY_VEHICLE(ped, false) then return nil end
    return N.GET_VEHICLE_PED_IS_IN and N.GET_VEHICLE_PED_IS_IN(ped, false)
end

local function delete_vehicle_by_handle(handle)
    if not handle or handle == 0 then return false end

    local success = pcall(function()
        if not N.DOES_ENTITY_EXIST or not N.DOES_ENTITY_EXIST(handle) then return end

        if request and request.control then
            request.control(handle, true)
        end

        if N.SET_ENTITY_AS_MISSION_ENTITY then
            N.SET_ENTITY_AS_MISSION_ENTITY(handle, false, true)
        end

        local ptr = pointer_int()
        ptr.value = handle
        invoker.call(0xEA386986E786A54F, ptr)
        ptr:free()
    end)

    return success
end

local function spawn_vehicle(model_name, display_name)
    if state.loading then
        ui.notify(_T("Spawning vehicle..."), "Warning")
        return
    end

    state.loading = true

    -- Save old vehicle handle for deletion
    local old_vehicle = state.delete_old_vehicle and state.last_spawned or nil

    -- Calculate hash
    local hash = joaat(model_name)

    util.create_job(function()
        -- Delete old vehicle
        if old_vehicle then
            delete_vehicle_by_handle(old_vehicle)
            state.last_spawned = nil
        end

        -- Load model
        if not request.model(hash) then
            ui.notify(_T("Model loading failed") .. ": " .. model_name, "Error")
            state.loading = false
            return
        end

        -- Get player info
        local ped = N.PLAYER_PED_ID and N.PLAYER_PED_ID()
        local coords = ped and N.GET_ENTITY_COORDS and N.GET_ENTITY_COORDS(ped, true)
        local heading = ped and N.GET_ENTITY_HEADING and N.GET_ENTITY_HEADING(ped)

        if not coords then
            ui.notify(_T("Cannot get player position"), "Error")
            if N.SET_MODEL_AS_NO_LONGER_NEEDED then
                N.SET_MODEL_AS_NO_LONGER_NEEDED(hash)
            end
            state.loading = false
            return
        end

        -- Calculate spawn position
        local spawn_x = coords.x - math_sin(math_rad(heading)) * 5.0
        local spawn_y = coords.y + math_cos(math_rad(heading)) * 5.0
        local spawn_z = coords.z

        -- Aircraft spawn in air
        if state.spawn_in_air then
            local is_aircraft = (N.IS_THIS_MODEL_A_PLANE and N.IS_THIS_MODEL_A_PLANE(hash)) or
                               (N.IS_THIS_MODEL_A_HELI and N.IS_THIS_MODEL_A_HELI(hash))
            if is_aircraft then
                spawn_z = spawn_z + 200.0  -- Spawn 200m above player
            end
        end

        -- Create vehicle
        local vehicle = N.CREATE_VEHICLE and N.CREATE_VEHICLE(
            hash, spawn_x, spawn_y, spawn_z, heading,
            true, true, false
        )

        if N.SET_MODEL_AS_NO_LONGER_NEEDED then
            N.SET_MODEL_AS_NO_LONGER_NEEDED(hash)
        end

        if not vehicle or vehicle == 0 then
            ui.notify(_T("Vehicle spawn failed"), "Error")
            state.loading = false
            return
        end

        state.last_spawned = vehicle

        -- Apply spawn options (wrapped in pcall to prevent loading state from getting stuck)
        if vehicle_customizer then
            pcall(function()
                -- Max performance upgrades
                if state.max_performance then
                    vehicle_customizer.apply_max_performance(vehicle)
                end

                -- Max visual upgrades
                if state.max_upgrade then
                    vehicle_customizer.apply_max_visual_upgrades(vehicle)
                end

                -- Clean vehicle
                if state.clean_vehicle then
                    vehicle_customizer.clean_vehicle(vehicle)
                end

                -- Bulletproof tires
                if state.bulletproof_tires then
                    vehicle_customizer.set_bulletproof_tires(vehicle, true)
                end

                -- Vehicle god mode
                if state.vehicle_godmode then
                    vehicle_customizer.set_invincible(vehicle, true)
                end

                -- Apply presets
                if state.use_preset then
                    -- Color presets
                    if state.color_mode == 1 then
                        -- Classic colors: use presets
                        local primary_preset = vehicle_customizer.PAINT_PRESETS[state.primary_color]
                        if primary_preset and primary_preset.value then
                            vehicle_customizer.set_custom_primary_color(vehicle, primary_preset.value.r, primary_preset.value.g, primary_preset.value.b)
                        end
                        local secondary_preset = vehicle_customizer.PAINT_PRESETS[state.secondary_color]
                        if secondary_preset and secondary_preset.value then
                            vehicle_customizer.set_custom_secondary_color(vehicle, secondary_preset.value.r, secondary_preset.value.g, secondary_preset.value.b)
                        end
                    elseif state.color_mode == 2 then
                        -- Custom RGB: use color picker values
                        vehicle_customizer.set_custom_primary_color(vehicle,
                            state.primary_color_rgb.r,
                            state.primary_color_rgb.g,
                            state.primary_color_rgb.b)
                        vehicle_customizer.set_custom_secondary_color(vehicle,
                            state.secondary_color_rgb.r,
                            state.secondary_color_rgb.g,
                            state.secondary_color_rgb.b)
                    else
                        -- Chameleon mode
                        local chameleon_preset = vehicle_customizer.CHAMELEON_PRESETS[state.chameleon_paint]
                        if chameleon_preset and chameleon_preset.id >= 161 then
                            vehicle_customizer.set_vehicle_color(vehicle, chameleon_preset.id, chameleon_preset.id)
                        end
                    end

                    -- Pearlescent and wheel colors
                    vehicle_customizer.set_pearlescent_color(vehicle, state.pearlescent_id)
                    vehicle_customizer.set_wheel_color(vehicle, state.wheel_color_id)

                    -- Light presets
                    local xenon_preset = vehicle_customizer.XENON_PRESETS[state.xenon_color]
                    if xenon_preset then
                        if xenon_preset.id == -1 then
                            vehicle_customizer.set_xenon_enabled(vehicle, false)
                        else
                            vehicle_customizer.set_xenon_color(vehicle, xenon_preset.id)
                        end
                    end

                    local neon_preset = vehicle_customizer.NEON_PRESETS[state.neon_color]
                    if neon_preset and neon_preset.value then
                        vehicle_customizer.set_neon_color(vehicle, neon_preset.value.r, neon_preset.value.g, neon_preset.value.b)
                        vehicle_customizer.set_all_neons_enabled(vehicle, true)
                    else
                        vehicle_customizer.set_all_neons_enabled(vehicle, false)
                    end

                    -- Wheel presets
                    local wheel_preset = vehicle_customizer.WHEEL_TYPES[state.wheel_type]
                    if wheel_preset then
                        vehicle_customizer.set_wheel_type(vehicle, wheel_preset.id)
                    end

                    -- Wheel style + custom tires
                    vehicle_customizer.set_front_wheels(vehicle, state.wheel_index, state.custom_tires)

                    local smoke_preset = vehicle_customizer.SMOKE_PRESETS[state.tire_smoke_color]
                    if smoke_preset and smoke_preset.value then
                        vehicle_customizer.set_tire_smoke_color(vehicle, smoke_preset.value.r, smoke_preset.value.g, smoke_preset.value.b)
                    else
                        vehicle_customizer.set_tire_smoke_enabled(vehicle, false)
                    end

                    -- Window tint
                    vehicle_customizer.set_window_tint(vehicle, state.window_tint - 1)

                    -- Plate style
                    vehicle_customizer.set_plate_type(vehicle, state.plate_type - 1)
                end

                -- Custom plate text (independent of presets)
                if state.plate_text and state.plate_text ~= "" then
                    vehicle_customizer.set_plate_text(vehicle, state.plate_text)
                end
            end)
        end

        -- Engine on
        if state.engine_on and N.SET_VEHICLE_ENGINE_ON then
            pcall(function()
                N.SET_VEHICLE_ENGINE_ON(vehicle, true, true, false)
            end)
        end

        -- Teleport to vehicle
        if state.teleport_to_vehicle and N.SET_PED_INTO_VEHICLE then
            N.SET_PED_INTO_VEHICLE(ped, vehicle, -1)
        end

        ui.notify(_T("Spawned") .. ": " .. display_name, "Success")
        state.loading = false
    end)
end

-- [UI: Vehicle Spawner Tab]
local function draw_vehicle_list()
    local query = state.search_query

    -- If search query exists, show results without categories
    if query and query ~= "" then
        local match_count = 0
        for _, category in ipairs(vehicles_data.CATEGORIES) do
            local vehicles = vehicles_data.VEHICLES[category]
            if vehicles then
                for i, vehicle in ipairs(vehicles) do
                    -- Match against localized name, original name, and model name
                    local display = vehicle.display_name or vehicle.name
                    if fuzzy_match(display, query) or fuzzy_match(vehicle.name, query) or fuzzy_match(vehicle.model, query) then
                        match_count = match_count + 1
                        if ui.Button(display .. "##search" .. category .. i) then
                            spawn_vehicle(vehicle.model, display)
                        end
                    end
                end
            end
        end
        if match_count == 0 then
            ui.Text(_T("No matching vehicles found"))
        end
    else
        -- No search query, show by category (using Collapsible component)
        for _, category in ipairs(vehicles_data.CATEGORIES) do
            local vehicles = vehicles_data.VEHICLES[category]
            if vehicles and #vehicles > 0 then
                local header = category .. " (" .. #vehicles .. ")"

                -- Use Collapsible component
                ui.Collapsible(header, function()
                    for i, vehicle in ipairs(vehicles) do
                        local display = vehicle.display_name or vehicle.name
                        if ui.Button(display .. "##" .. category .. i) then
                            spawn_vehicle(vehicle.model, display)
                        end
                    end
                end)
            end
        end
    end
end

--- Spawn external vehicle (auto-detect format)
---@param vehicle_data table Vehicle data from external file
local function spawn_external_vehicle(vehicle_data)
    if state.loading then
        ui.notify(_T("Spawning vehicle..."), "Warning")
        return
    end

    state.loading = true

    -- Auto-detect format and determine model hash
    local hash
    if vehicle_data.format == "Cherax" or vehicle_data.format == "Lexis" then
        hash = vehicle_data.model  -- Cherax and Lexis use hash directly
    else
        hash = joaat(vehicle_data.model)  -- Stand uses string model names
    end

    -- Save old vehicle for deletion
    local old_vehicle = state.delete_old_vehicle and state.last_spawned or nil

    util.create_job(function()
        -- Delete old vehicle
        if old_vehicle then
            delete_vehicle_by_handle(old_vehicle)
            state.last_spawned = nil
        end

        -- Load model
        if not request.model(hash) then
            ui.notify(_T("Model loading failed") .. ": " .. vehicle_data.name, "Error")
            state.loading = false
            return
        end

        -- Get player info
        local ped = N.PLAYER_PED_ID and N.PLAYER_PED_ID()
        local coords = ped and N.GET_ENTITY_COORDS and N.GET_ENTITY_COORDS(ped, true)
        local heading = ped and N.GET_ENTITY_HEADING and N.GET_ENTITY_HEADING(ped)

        if not coords then
            ui.notify(_T("Cannot get player position"), "Error")
            if N.SET_MODEL_AS_NO_LONGER_NEEDED then
                N.SET_MODEL_AS_NO_LONGER_NEEDED(hash)
            end
            state.loading = false
            return
        end

        -- Calculate spawn position
        local spawn_x = coords.x - math_sin(math_rad(heading)) * 5.0
        local spawn_y = coords.y + math_cos(math_rad(heading)) * 5.0
        local spawn_z = coords.z

        -- Aircraft spawn in air
        if state.spawn_in_air then
            local is_aircraft = (N.IS_THIS_MODEL_A_PLANE and N.IS_THIS_MODEL_A_PLANE(hash)) or
                               (N.IS_THIS_MODEL_A_HELI and N.IS_THIS_MODEL_A_HELI(hash))
            if is_aircraft then
                spawn_z = spawn_z + 200.0
            end
        end

        -- Create vehicle
        local vehicle = N.CREATE_VEHICLE and N.CREATE_VEHICLE(
            hash, spawn_x, spawn_y, spawn_z, heading,
            true, true, false
        )

        if N.SET_MODEL_AS_NO_LONGER_NEEDED then
            N.SET_MODEL_AS_NO_LONGER_NEEDED(hash)
        end

        if not vehicle or vehicle == 0 then
            ui.notify(_T("Vehicle spawn failed"), "Error")
            state.loading = false
            return
        end

        state.last_spawned = vehicle

        -- Wait for mods to stream in
        local timeout = 0
        while timeout < 100 do
            if N.HAVE_VEHICLE_MODS_STREAMED_IN and
               N.HAVE_VEHICLE_MODS_STREAMED_IN(vehicle) then
                break
            end
            timeout = timeout + 1
            util.yield(10)
        end

        -- Apply external vehicle mods (auto-detect format)
        if vehicle_customizer then
            pcall(function()
                if vehicle_data.format == "Stand" then
                    vehicle_customizer.apply_stand_mods(vehicle, vehicle_data.data)
                elseif vehicle_data.format == "Lexis" then
                    vehicle_customizer.apply_lexis_mods(vehicle, vehicle_data.data)
                elseif vehicle_data.format == "Cherax" then
                    vehicle_customizer.apply_cherax_mods(vehicle, vehicle_data.data)
                end

                -- Clean vehicle
                vehicle_customizer.clean_vehicle(vehicle)

                -- Apply presets (overrides external vehicle settings)
                if state.use_preset then
                    -- Color presets
                    if state.color_mode == 1 then
                        -- Classic colors: use presets
                        local primary_preset = vehicle_customizer.PAINT_PRESETS[state.primary_color]
                        if primary_preset and primary_preset.value then
                            vehicle_customizer.set_custom_primary_color(vehicle, primary_preset.value.r, primary_preset.value.g, primary_preset.value.b)
                        end
                        local secondary_preset = vehicle_customizer.PAINT_PRESETS[state.secondary_color]
                        if secondary_preset and secondary_preset.value then
                            vehicle_customizer.set_custom_secondary_color(vehicle, secondary_preset.value.r, secondary_preset.value.g, secondary_preset.value.b)
                        end
                    elseif state.color_mode == 2 then
                        -- Custom RGB: use color picker values
                        vehicle_customizer.set_custom_primary_color(vehicle,
                            state.primary_color_rgb.r,
                            state.primary_color_rgb.g,
                            state.primary_color_rgb.b)
                        vehicle_customizer.set_custom_secondary_color(vehicle,
                            state.secondary_color_rgb.r,
                            state.secondary_color_rgb.g,
                            state.secondary_color_rgb.b)
                    else
                        -- Chameleon mode
                        local chameleon_preset = vehicle_customizer.CHAMELEON_PRESETS[state.chameleon_paint]
                        if chameleon_preset and chameleon_preset.id >= 161 then
                            vehicle_customizer.set_vehicle_color(vehicle, chameleon_preset.id, chameleon_preset.id)
                        end
                    end

                    vehicle_customizer.set_pearlescent_color(vehicle, state.pearlescent_id)
                    vehicle_customizer.set_wheel_color(vehicle, state.wheel_color_id)

                    -- Light presets
                    local xenon_preset = vehicle_customizer.XENON_PRESETS[state.xenon_color]
                    if xenon_preset then
                        if xenon_preset.id == -1 then
                            vehicle_customizer.set_xenon_enabled(vehicle, false)
                        else
                            vehicle_customizer.set_xenon_color(vehicle, xenon_preset.id)
                        end
                    end

                    local neon_preset = vehicle_customizer.NEON_PRESETS[state.neon_color]
                    if neon_preset and neon_preset.value then
                        vehicle_customizer.set_neon_color(vehicle, neon_preset.value.r, neon_preset.value.g, neon_preset.value.b)
                        vehicle_customizer.set_all_neons_enabled(vehicle, true)
                    else
                        vehicle_customizer.set_all_neons_enabled(vehicle, false)
                    end

                    -- Wheel presets
                    local wheel_preset = vehicle_customizer.WHEEL_TYPES[state.wheel_type]
                    if wheel_preset then
                        vehicle_customizer.set_wheel_type(vehicle, wheel_preset.id)
                    end

                    -- Wheel style + custom tires
                    vehicle_customizer.set_front_wheels(vehicle, state.wheel_index, state.custom_tires)

                    local smoke_preset = vehicle_customizer.SMOKE_PRESETS[state.tire_smoke_color]
                    if smoke_preset and smoke_preset.value then
                        vehicle_customizer.set_tire_smoke_color(vehicle, smoke_preset.value.r, smoke_preset.value.g, smoke_preset.value.b)
                    else
                        vehicle_customizer.set_tire_smoke_enabled(vehicle, false)
                    end

                    -- Window tint
                    vehicle_customizer.set_window_tint(vehicle, state.window_tint - 1)

                    -- Plate style
                    vehicle_customizer.set_plate_type(vehicle, state.plate_type - 1)
                end

                -- Custom plate text
                if state.plate_text and state.plate_text ~= "" then
                    vehicle_customizer.set_plate_text(vehicle, state.plate_text)
                end
            end)
        end

        -- Engine on
        if state.engine_on and N.SET_VEHICLE_ENGINE_ON then
            pcall(function()
                N.SET_VEHICLE_ENGINE_ON(vehicle, true, true, false)
            end)
        end

        -- Teleport to vehicle
        if state.teleport_to_vehicle and N.SET_PED_INTO_VEHICLE and ped then
            N.SET_PED_INTO_VEHICLE(ped, vehicle, -1)
        end

        state.loading = false
        ui.notify(string.format(_T("Vehicle spawned successfully") .. " [%s]: %s",
            vehicle_data.format, vehicle_data.name), "Success")
    end)
end

local function draw_settings()
    ui.Group(_T("Spawn Options"), function()
        -- Basic options (row layout)
        ui.Row({
            function()
                state.teleport_to_vehicle = ui.Checkbox(_T("Teleport to Vehicle"), state.teleport_to_vehicle)
            end,
            function()
                state.delete_old_vehicle = ui.Checkbox(_T("Delete Old Vehicle"), state.delete_old_vehicle)
            end,
        })

        ui.Row({
            function()
                state.spawn_in_air = ui.Checkbox(_T("Aircraft Spawn in Air"), state.spawn_in_air)
            end,
            function()
                state.engine_on = ui.Checkbox(_T("Engine On"), state.engine_on)
            end,
        })

        ui.Row({
            function()
                state.clean_vehicle = ui.Checkbox(_T("Clean Vehicle"), state.clean_vehicle)
            end,
            function()
                state.bulletproof_tires = ui.Checkbox(_T("Bulletproof Tires"), state.bulletproof_tires)
            end,
        })

        -- Vehicle upgrade options
        ui.Row({
            function()
                state.max_performance = ui.Checkbox(_T("Max Performance"), state.max_performance)
            end,
            function()
                state.max_upgrade = ui.Checkbox(_T("Max Upgrade"), state.max_upgrade)
            end,
        })

        ui.Row({
            function()
                state.vehicle_godmode = ui.Checkbox(_T("Vehicle God Mode"), state.vehicle_godmode)
            end,
            function()
                if ui.Button(_T("Delete Current Vehicle")) then
                    local vehicle = get_current_vehicle()
                    if vehicle and vehicle ~= 0 then
                        if delete_vehicle_by_handle(vehicle) then
                            -- If deleted vehicle is last spawned, clear record
                            if vehicle == state.last_spawned then
                                state.last_spawned = nil
                            end
                            ui.notify(_T("Vehicle deleted"), "Info")
                        end
                    else
                        ui.notify(_T("Please enter a vehicle first"), "Warning")
                    end
                end
            end,
        })
    end)

    -- Preset options (separate Group)
    if vehicle_customizer and cached_options.paint_names then
        -- Enable presets option (above color section)
        state.use_preset = ui.Checkbox(_T("Enable Presets"), state.use_preset)

        -- Color presets
        ui.Group(_T("Color"), function()
            local new_mode = ui.Combo(_T("Color Mode"), state.color_mode, cached_options.color_modes)
            if new_mode ~= state.color_mode then
                state.color_mode = new_mode
            end

            if state.color_mode == 1 then
                -- Classic colors: primary + secondary side by side (preset selection)
                ui.Row({
                    function()
                        local new_primary = ui.Combo(_T("Primary"), state.primary_color, cached_options.paint_names)
                        if new_primary ~= state.primary_color then
                            state.primary_color = new_primary
                        end
                    end,
                    function()
                        local new_secondary = ui.Combo(_T("Secondary"), state.secondary_color, cached_options.paint_names)
                        if new_secondary ~= state.secondary_color then
                            state.secondary_color = new_secondary
                        end
                    end,
                })
            elseif state.color_mode == 2 then
                -- Custom RGB: use color picker
                ui.Row({
                    function()
                        ui.ColorPicker(_T("Primary"), state, "primary_color_rgb")
                    end,
                    function()
                        ui.ColorPicker(_T("Secondary"), state, "secondary_color_rgb")
                    end,
                })
            else
                -- Chameleon mode
                local new_chameleon = ui.Combo(_T("Chameleon Paint"), state.chameleon_paint, cached_options.chameleon_names)
                if new_chameleon ~= state.chameleon_paint then
                    state.chameleon_paint = new_chameleon
                end
            end

            -- Pearlescent + wheel color side by side
            ui.Row({
                function()
                    local new_pearl = ui.Slider(_T("Pearlescent"), state.pearlescent_id, 0, 159, 1)
                    if new_pearl ~= state.pearlescent_id then
                        state.pearlescent_id = new_pearl
                    end
                end,
                function()
                    local new_wheel_color = ui.Slider(_T("Wheel Color"), state.wheel_color_id, 0, 159, 1)
                    if new_wheel_color ~= state.wheel_color_id then
                        state.wheel_color_id = new_wheel_color
                    end
                end,
            })
        end)

        -- Light presets
        ui.Group(_T("Lights"), function()
            ui.Row({
                function()
                    local new_xenon = ui.Combo(_T("Xenon Lights"), state.xenon_color, cached_options.xenon_names)
                    if new_xenon ~= state.xenon_color then
                        state.xenon_color = new_xenon
                    end
                end,
                function()
                    local new_neon = ui.Combo(_T("Neon Lights"), state.neon_color, cached_options.neon_names)
                    if new_neon ~= state.neon_color then
                        state.neon_color = new_neon
                    end
                end,
            })
        end)

        -- Wheel presets
        ui.Group(_T("Wheels"), function()
            -- Wheel type
            local new_wheel_type = ui.Combo(_T("Wheel Type"), state.wheel_type, cached_options.wheel_type_names)
            if new_wheel_type ~= state.wheel_type then
                state.wheel_type = new_wheel_type
            end

            -- Wheel style
            local new_wheel_index = ui.Slider(_T("Wheel Style") .. " (0-216)", state.wheel_index, 0, 216, 1)
            if new_wheel_index ~= state.wheel_index then
                state.wheel_index = new_wheel_index
            end

            -- Custom tires and tire smoke side by side
            ui.Row({
                function()
                    local new_custom_tires = ui.Checkbox(_T("Custom Tires"), state.custom_tires)
                    if new_custom_tires ~= state.custom_tires then
                        state.custom_tires = new_custom_tires
                    end
                end,
                function()
                    local new_smoke = ui.Combo(_T("Tire Smoke"), state.tire_smoke_color, cached_options.smoke_names)
                    if new_smoke ~= state.tire_smoke_color then
                        state.tire_smoke_color = new_smoke
                    end
                end,
            })
        end)

        -- Window presets
        ui.Group(_T("Windows"), function()
            local new_tint = ui.Combo(_T("Window Tint"), state.window_tint, vehicle_customizer.WINDOW_TINTS)
            if new_tint ~= state.window_tint then
                state.window_tint = new_tint
            end
        end)

        -- Plate presets
        ui.Group(_T("License Plate"), function()
            -- Plate style
            local new_plate_type = ui.Combo(_T("Plate Style"), state.plate_type, vehicle_customizer.PLATE_TYPES)
            if new_plate_type ~= state.plate_type then
                state.plate_type = new_plate_type
            end

            -- Plate text
            local new_plate = ui.TextInput(_T("Plate Text"), state.plate_text, 8)
            if new_plate ~= state.plate_text then
                state.plate_text = new_plate
            end
        end)
    end
end

local function draw_spawner_tab()
    -- Dual column layout: left for spawn options, right for vehicle list
    ui.DualColumn(
        function()
            -- Left column: spawn options
            draw_settings()
        end,
        function()
            -- Right column: search box + vehicle list
            -- Search box
            local new_query = ui.TextInput(_T("Search Vehicles"), state.search_query, 32)
            if new_query ~= state.search_query then
                state.search_query = new_query
            end

            -- Vehicle list
            draw_vehicle_list()
        end
    )
end

-- [UI: Custom Vehicles Tab (Stand + Cherax + Lexis)]
--- Group vehicles by format and apply search filter
local function get_grouped_vehicles()
    local groups = {
        Lexis = {},
        Stand = {},
        Cherax = {}
    }

    for _, veh in ipairs(state.external_vehicles) do
        -- Apply search filter
        if fuzzy_match(veh.name, state.search_text) then
            if groups[veh.format] then
                table.insert(groups[veh.format], veh)
            end
        end
    end

    return groups
end

--- Save current vehicle in Lexis format
---@param filename string Filename (without extension)
local function save_current_vehicle_as_lexis(filename)
    if not filename or filename == "" then
        ui.notify(_T("Please enter a filename"), "Warning")
        return
    end

    -- Get current vehicle
    local vehicle = get_current_vehicle()
    if not vehicle or vehicle == 0 then
        ui.notify(_T("Please enter a vehicle first"), "Warning")
        return
    end

    -- Check file API
    if not file or not file.open then
        ui.notify(_T("file API not available"), "Error")
        return
    end

    -- Get save path
    local base = (paths and paths.script) or ".\\"
    if not base:match("[\\/]$") then base = base .. "\\" end
    local save_path = base .. "ClickUl\\Vehicles\\Lexis\\" .. filename .. ".json"

    if debugger then debugger.debug("[Spawn] Saving vehicle to: " .. save_path) end

    -- Use VehicleCustomizer to export vehicle data
    if not vehicle_customizer or not vehicle_customizer.export_vehicle_data then
        ui.notify(_T("VehicleCustomizer export feature not available"), "Error")
        if debugger then
            debugger.error("[Spawn] VehicleCustomizer export_vehicle_data not available")
        end
        return
    end

    -- Export vehicle data
    local ok_export, vehicle_data = pcall(function()
        return vehicle_customizer.export_vehicle_data(vehicle)
    end)

    if not ok_export or not vehicle_data then
        ui.notify(_T("Failed to export vehicle data"), "Error")
        if debugger then
            debugger.error("[Spawn] Failed to export vehicle data: " .. tostring(vehicle_data))
        end
        return
    end

    -- Convert to JSON (use pretty print for readability)
    local ok_json, json_str = pcall(json.encode_pretty, vehicle_data)
    if not ok_json or not json_str then
        ui.notify(_T("JSON encoding failed"), "Error")
        if debugger then
            debugger.error("[Spawn] Failed to encode JSON: " .. tostring(json_str))
        end
        return
    end

    -- Save file
    local ok_save, err_save = pcall(function()
        local f = file.open(save_path, {create_if_not_exists=true, append=false})
        if f and f.valid then
            f.text = json_str
            if f.close then
                pcall(f.close, f)
            elseif f.Close then
                pcall(f.Close, f)
            end
            return true
        end
        return false, "Failed to open file"
    end)

    if ok_save and err_save then
        ui.notify(_T("Vehicle saved") .. ": " .. filename, "Success")
        if debugger then
            debugger.info("[Spawn] Vehicle saved successfully: " .. save_path)
        end
        -- Refresh list
        scan_vehicle_files()
    else
        ui.notify(_T("Failed to save file"), "Error")
        if debugger then
            debugger.error("[Spawn] Failed to save file: " .. tostring(err_save))
        end
    end
end

--- Scan all vehicle files
scan_vehicle_files = function()
    if debugger then debugger.info("[Spawn] Auto-scanning vehicle files...") end
    state.external_vehicles = {}
    state.external_loaded = false

    -- Get paths
    local base = (paths and paths.script) or ".\\"
    if not base:match("[\\/]$") then base = base .. "\\" end
    local vehicles_path = base .. "ClickUl\\Vehicles\\"
    local stand_path = vehicles_path .. "Stand\\"
    local cherax_path = vehicles_path .. "Cherax\\"
    local lexis_path = vehicles_path .. "Lexis\\"

    if not dirs or not dirs.list then
        ui.notify(_T("dirs API not available"), "Error")
        return
    end

    -- Scan Stand files
    local ok_stand, stand_files = pcall(dirs.list, stand_path, "txt")
    if ok_stand and stand_files then
        for _, file_path in ipairs(stand_files) do
            local ok_parse, data = pcall(parse_stand_file, file_path)
            if ok_parse and data then
                local model_name = data["Model Name"] or data["Model"] or data["model"]
                if model_name then
                    local file_name = file_path:match("[\\/]([^\\/]+)%.txt$") or "Unknown"
                    table.insert(state.external_vehicles, {
                        name = model_name,
                        display_name = file_name,
                        path = file_path,
                        model = model_name,
                        data = data,
                        format = "Stand",
                    })
                end
            end
        end
    end

    -- Scan Cherax files
    local ok_cherax, cherax_files = pcall(dirs.list, cherax_path, "json")
    if ok_cherax and cherax_files then
        for _, file_path in ipairs(cherax_files) do
            local ok_parse, data = pcall(parse_cherax_file, file_path)
            if ok_parse and data and data.model then
                local file_name = file_path:match("[\\/]([^\\/]+)%.json$") or "Unknown"
                table.insert(state.external_vehicles, {
                    name = data.name or file_name,
                    display_name = file_name,
                    path = file_path,
                    model = data.model,
                    data = data,
                    format = "Cherax",
                })
            end
        end
    end

    -- Scan Lexis files
    local ok_lexis, lexis_files = pcall(dirs.list, lexis_path, "json")
    if ok_lexis and lexis_files then
        for _, file_path in ipairs(lexis_files) do
            local ok_parse, data = pcall(parse_lexis_file, file_path)
            if ok_parse and data and data.model then
                local file_name = file_path:match("[\\/]([^\\/]+)%.json$") or "Unknown"
                table.insert(state.external_vehicles, {
                    name = file_name:gsub("%.json$", ""),
                    display_name = file_name,
                    path = file_path,
                    model = data.model,
                    data = data,
                    format = "Lexis",
                })
            end
        end
    end

    state.external_loaded = true
    ui.notify(_T("Scan complete") .. ": " .. #state.external_vehicles .. " " .. _T("vehicles"), "Success")
end

--- Get safe display name (prevent passing function causing renderer error)
local function get_safe_name(veh)
    local name = veh.name
    if type(name) == "function" then
        name = veh.display_name or veh.model or "Unknown"
    end
    return tostring(name or "Unknown")
end

--- Draw custom vehicle list (same pattern as built-in vehicles)
local function draw_custom_vehicle_list()
    local query = state.search_text

    -- If search query exists, show results without categories (flat list)
    if query and query ~= "" then
        local match_count = 0
        for i, veh in ipairs(state.external_vehicles) do
            local name = get_safe_name(veh)
            -- Match both name and model
            if fuzzy_match(name, query) or fuzzy_match(tostring(veh.model), query) then
                match_count = match_count + 1
                if ui.Button(name .. "##ext_" .. i) then
                    spawn_external_vehicle(veh)
                end
            end
        end
        if match_count == 0 then
            ui.Text(_T("No matching vehicles found"))
        end
    else
        -- No search query, show by format categories (using Collapsible component)
        local groups = get_grouped_vehicles()

        -- Lexis group
        if #groups.Lexis > 0 then
            ui.Collapsible("Lexis (" .. #groups.Lexis .. ")", function()
                for i, veh in ipairs(groups.Lexis) do
                    local name = get_safe_name(veh)
                    if ui.Button(name .. "##lexis_" .. i) then
                        spawn_external_vehicle(veh)
                    end
                end
            end)
        end

        -- Stand group
        if #groups.Stand > 0 then
            ui.Collapsible("Stand (" .. #groups.Stand .. ")", function()
                for i, veh in ipairs(groups.Stand) do
                    local name = get_safe_name(veh)
                    if ui.Button(name .. "##stand_" .. i) then
                        spawn_external_vehicle(veh)
                    end
                end
            end)
        end

        -- Cherax group
        if #groups.Cherax > 0 then
            ui.Collapsible("Cherax (" .. #groups.Cherax .. ")", function()
                for i, veh in ipairs(groups.Cherax) do
                    local name = get_safe_name(veh)
                    if ui.Button(name .. "##cherax_" .. i) then
                        spawn_external_vehicle(veh)
                    end
                end
            end)
        end

        -- No vehicles
        if #groups.Lexis == 0 and #groups.Stand == 0 and #groups.Cherax == 0 then
            if not state.external_loaded then
                ui.Text(_T("Click 'Refresh' on the left to scan vehicles"))
            else
                ui.Text(_T("No vehicle files found"))
            end
        end
    end
end

--- Draw custom vehicles tab (Stand + Cherax + Lexis unified)
local function draw_custom_vehicles()
    -- Use dual column layout: same pattern as built-in vehicles
    ui.DualColumn(
        function()
            -- Left column: save and refresh functions
            ui.Group(_T("Save Current Vehicle"), function()
                -- Save vehicle
                local new_name = ui.TextInput(_T("Filename") .. "##save", state.save_vehicle_name, 32)
                if new_name ~= state.save_vehicle_name then
                    state.save_vehicle_name = new_name
                end

                if ui.Button(_T("Save as Lexis Format")) then
                    save_current_vehicle_as_lexis(state.save_vehicle_name)
                end

                -- Refresh list
                if ui.Button(_T("Refresh Vehicle List")) then
                    scan_vehicle_files()
                end
            end)
        end,
        function()
            -- Right column: search box + vehicle list
            local new_search = ui.TextInput(_T("Search Vehicles") .. "##custom", state.search_text, 32)
            if new_search ~= state.search_text then
                state.search_text = new_search
            end

            -- Vehicle list
            draw_custom_vehicle_list()
        end
    )
end

-- [Main Draw]
function module.draw()
    if not vehicles_data then
        ui.Text(_T("Vehicle data not loaded"))
        return
    end

    -- Main tabs (SubTabBar)
    state.main_tab = ui.SubTabBar(cached_options.main_tabs, state.main_tab)

    -- Tab content
    if state.main_tab == 1 then
        draw_spawner_tab()
    elseif state.main_tab == 2 then
        draw_custom_vehicles()
    end
end

function module.on_menu_close()
    save_config()
end

-- [Frame Update]
function module.on_tick()
end

return module
